"""
RAG (检索增强生成) 服务封装 - 完整14步流程实现
支持知识库初始化、向量检索、关键词+语义混合检索等功能
"""
import os
import re
import json
import glob
import hashlib
import logging
from pathlib import Path
from typing import List, Optional, Dict, Any, Tuple
from operator import itemgetter
from datetime import datetime
from langchain_chroma import Chroma
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import (
    TextLoader, PyPDFLoader, JSONLoader, UnstructuredHTMLLoader
)
from dashscope import Generation

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)



# 大语言模型包装类
class DashScopeLLM:
    """DashScope LLM包装类，兼容langchain接口"""
    
    def __init__(self, api_key: str, model: str = "qwen-max"):
        self.api_key = api_key
        self.model = model
    
    def invoke(self, prompt: str) -> str:
        # 将prompt转换为messages格式
        messages = [
            {"role": "user", "content": prompt}
        ]

        # 调用大语言模型
        response = Generation.call(
            api_key=self.api_key,
            model=self.model,
            messages=messages,
            result_format="message"
        )

        # 如果调用成功，返回回答
        if response.status_code == 200:
            return response.output.choices[0].message.content
        else:
            raise Exception(f"DashScope API调用失败: {response.message}")



class RAGService:
    """RAG服务类，封装知识库检索和问答功能"""
    def __init__(
        self,
        knowledge_dir: str = "my_knowledge",  # 知识库文件目录
        persist_directory: str = "./chroma_v4",# 向量数据库持久化目录
        chunk_size: int = 500, # 文档分块大小
        chunk_overlap: int = 100, # 文档块重叠大小
        retrieval_k: int = 5, # 检索返回的文档数量
        api_key: Optional[str] = None, # 阿里云DashScope API Key
        embedding_api_key: Optional[str] = None, # 阿里云DashScope API Key（Embedding）
        model: str = "qwen-max", # 大语言模型名称
        embedding_model: str = "text-embedding-v4", # 向量模型名称
        similarity_threshold: float = 0.5  # 相似度阈值（默认0.5，范围0-1，越大越严格）
    ):

        # 获取API Key
        self.api_key = api_key or os.getenv("DASHSCOPE_API_KEY", "")
        self.embedding_api_key = embedding_api_key or os.getenv("DASHSCOPE_API_KEY", "")
        self.model = model
        
        # 初始化大语言模型（使用DashScope SDK）
        self.llm = DashScopeLLM(api_key=self.api_key, model=model)
        
        # 初始化文本嵌入模型
        self.embeddings_model = DashScopeEmbeddings(
            model=embedding_model,
            dashscope_api_key=self.embedding_api_key
        )
        
        # 知识库目录
        self.knowledge_dir = Path(knowledge_dir)
        
        # 文本分割器
        self.text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=chunk_overlap
        )
        
        # 初始化向量存储
        self.vector_store = Chroma(
            embedding_function=self.embeddings_model,
            persist_directory=persist_directory
        )
        
        # 创建检索器
        self.retriever = self.vector_store.as_retriever(
            search_kwargs={"k": retrieval_k}
        )
        
        # 提示模板
        self.prompt_template = PromptTemplate.from_template("""
你是一个智能问答客服机器人，请根据以下上下文信息回答用户的问题。
如果上下文中没有相关信息，请礼貌地告知用户，并尽量提供帮助。

上下文信息：
{context}

用户问题：{question}

请提供准确、友好的回答：
""")
        
        # 构建处理链 - 使用RunnableLambda包装DashScopeLLM以兼容langchain
        def _invoke_llm(prompt_value):
            """将PromptValue转换为字符串后调用LLM"""
            if hasattr(prompt_value, 'to_string'):
                prompt_str = prompt_value.to_string()
            elif isinstance(prompt_value, str):
                prompt_str = prompt_value
            else:
                prompt_str = str(prompt_value)
            return self.llm.invoke(prompt_str)
        
        llm_runnable = RunnableLambda(_invoke_llm)
        
        self.chain = (
            {"question": RunnablePassthrough()}  # 将问题传递给下一个节点
            | RunnablePassthrough.assign(   # 将问题和检索结果合并到上下文中
                context=itemgetter("question") | self.retriever  # 将问题和检索结果合并到上下文中
            )
            | self.prompt_template  # 将上下文和问题合并到提示中
            | llm_runnable  # 调用大语言模型（使用RunnableLambda包装）
            | StrOutputParser()  # 解析输出为字符串
        )
        
        # 文档哈希集合，用于去重（步骤2、6）
        self.document_hashes: set = set()
        # 相关性阈值（步骤10）
        self.similarity_threshold = similarity_threshold
        # 是否启用问题预处理（步骤7）
        self.enable_query_preprocessing = True
        # 是否启用结果优化（步骤13）
        self.enable_result_optimization = True


    # ========== 步骤1-2：原始数据采集和数据清洗预处理 ==========
    
    def _clean_text(self, text: str) -> str:
        """
        步骤2：数据清洗与预处理
        - 去噪：剔除广告、页眉页脚、乱码
        - 格式统一：统一换行符、空格等
        """
        if not text:
            return ""
        
        # 去除多余的空白字符
        text = re.sub(r'\s+', ' ', text)
        
        # 去除常见的页眉页脚模式
        text = re.sub(r'第\s*\d+\s*页', '', text)
        text = re.sub(r'共\s*\d+\s*页', '', text)
        
        # 去除乱码（非中英文数字和常见标点）
        text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9\s，。！？；：、""''（）【】《》\-\.,!?;:()\[\]<>]', '', text)
        
        # 去除重复的标点符号
        text = re.sub(r'([，。！？；：])\1+', r'\1', text)
        
        return text.strip()
    
    def _calculate_document_hash(self, content: str) -> str:
        """计算文档内容的哈希值，用于去重"""
        return hashlib.md5(content.encode('utf-8')).hexdigest()
    
    def _is_duplicate(self, content: str) -> bool:
        """检查文档是否重复（步骤2：去重）"""
        doc_hash = self._calculate_document_hash(content)
        if doc_hash in self.document_hashes:
            return True
        self.document_hashes.add(doc_hash)
        return False
    
    def load_knowledge_base(self, file_pattern: str = "*.txt", encoding: str = "utf-8") -> List[Document]:
        """
        步骤1：原始数据采集 - 加载文本文件
        Args:
            file_pattern: 文件匹配模式，默认"*.txt"
            encoding: 文件编码，默认"utf-8"
        
        Returns:
            加载的文档列表
        """
        if not self.knowledge_dir.exists():
            self.knowledge_dir.mkdir(parents=True, exist_ok=True)
            return []
        
        pattern = str(self.knowledge_dir / file_pattern)
        text_files = glob.glob(pattern)
        docs = []
        
        for file_path in text_files:
            try:
                loader = TextLoader(file_path, encoding=encoding)
                documents = loader.load()
                for doc in documents:
                    # 步骤2：数据清洗
                    cleaned_content = self._clean_text(doc.page_content)
                    if cleaned_content and not self._is_duplicate(cleaned_content):
                        doc.page_content = cleaned_content
                        doc.metadata = doc.metadata or {}
                        doc.metadata.update({
                            "source": file_path,
                            "file_type": "text",
                            "load_time": datetime.now().isoformat()
                        })
                        docs.append(doc)
            except Exception as e:
                logger.error(f"读取文本文件 {file_path} 出错: {e}")
        
        return docs
    
    def load_excel_files(self, file_pattern: str) -> List:
        """
        加载Excel文件

        Args:
            file_pattern: 文件匹配模式，如"*.xlsx"或"*.xls"

        Returns:
            加载的文档列表
        """
        import glob
        from langchain_core.documents import Document
        import pandas as pd

        if not self.knowledge_dir.exists():
            self.knowledge_dir.mkdir(parents=True, exist_ok=True)
            return []

        # 查找匹配的Excel文件
        pattern = str(self.knowledge_dir / file_pattern)
        excel_files = glob.glob(pattern)
        docs = []

        for file_path in excel_files:
            try:
                # 读取Excel文件
                xls = pd.ExcelFile(file_path)
                for sheet_name in xls.sheet_names:
                    df = pd.read_excel(xls, sheet_name=sheet_name)
                    # 将DataFrame转换为文本
                    content = df.to_string(index=False)
                    # 步骤2：数据清洗
                    cleaned_content = self._clean_text(content)
                    if cleaned_content and not self._is_duplicate(cleaned_content):
                        docs.append(Document(
                            page_content=cleaned_content, 
                            metadata={
                                "source": file_path, 
                                "sheet": sheet_name,
                                "file_type": "excel",
                                "load_time": datetime.now().isoformat()
                            }
                        ))
            except Exception as e:
                logger.error(f"读取Excel文件 {file_path} 出错: {e}")

        return docs

    # 步骤1：原始数据采集 - 加载PDF文件
    def load_pdf_files(self, file_pattern: str = "*.pdf") -> List[Document]:
        """
        步骤1：原始数据采集 - 加载PDF文件
        
        Args:
            file_pattern: 文件匹配模式，默认"*.pdf"
        
        Returns:
            加载的文档列表
        """
        if not self.knowledge_dir.exists():
            return []
        
        pattern = str(self.knowledge_dir / file_pattern)
        pdf_files = glob.glob(pattern)
        docs = []
        
        for file_path in pdf_files:
            try:
                loader = PyPDFLoader(file_path)
                documents = loader.load()
                for doc in documents:
                    # 步骤2：数据清洗
                    cleaned_content = self._clean_text(doc.page_content)
                    if cleaned_content and not self._is_duplicate(cleaned_content):
                        doc.page_content = cleaned_content
                        doc.metadata = doc.metadata or {}
                        doc.metadata.update({
                            "source": file_path,
                            "file_type": "pdf",
                            "load_time": datetime.now().isoformat()
                        })
                        docs.append(doc)
            except Exception as e:
                logger.error(f"读取PDF文件 {file_path} 出错: {e}")
        
        return docs
    # 步骤1：原始数据采集 - 加载JSON文件
    def load_json_files(self, file_pattern: str = "*.json") -> List[Document]:
        """
        步骤1：原始数据采集 - 加载JSON文件
        
        Args:
            file_pattern: 文件匹配模式，默认"*.json"
        
        Returns:
            加载的文档列表
        """
        if not self.knowledge_dir.exists():
            return []
        
        pattern = str(self.knowledge_dir / file_pattern)
        json_files = glob.glob(pattern)
        docs = []
        
        for file_path in json_files:
            try:
                # JSONLoader需要指定jq_schema来提取内容
                loader = JSONLoader(
                    file_path=file_path,
                    jq_schema='.',  # 提取整个JSON
                    text_content=False
                )
                documents = loader.load()
                for doc in documents:
                    # 步骤2：数据清洗
                    cleaned_content = self._clean_text(doc.page_content)
                    if cleaned_content and not self._is_duplicate(cleaned_content):
                        doc.page_content = cleaned_content
                        doc.metadata = doc.metadata or {}
                        doc.metadata.update({
                            "source": file_path,
                            "file_type": "json",
                            "load_time": datetime.now().isoformat()
                        })
                        docs.append(doc)
            except Exception as e:
                logger.error(f"读取JSON文件 {file_path} 出错: {e}")
        
        return docs

    # HTML文件加载
    def load_html_files(self, file_pattern: str = "*.html") -> List[Document]:
        """
        步骤1：原始数据采集 - 加载HTML/网页文件
        
        Args:
            file_pattern: 文件匹配模式，默认"*.html"
        
        Returns:
            加载的文档列表
        """
        if not self.knowledge_dir.exists():
            return []
        
        pattern = str(self.knowledge_dir / file_pattern)
        html_files = glob.glob(pattern)
        docs = []
        
        for file_path in html_files:
            try:
                loader = UnstructuredHTMLLoader(file_path)
                documents = loader.load()
                for doc in documents:
                    # 步骤2：数据清洗
                    cleaned_content = self._clean_text(doc.page_content)
                    if cleaned_content and not self._is_duplicate(cleaned_content):
                        doc.page_content = cleaned_content
                        doc.metadata = doc.metadata or {}
                        doc.metadata.update({
                            "source": file_path,
                            "file_type": "html",
                            "load_time": datetime.now().isoformat()
                        })
                        docs.append(doc)
            except Exception as e:
                logger.error(f"读取HTML文件 {file_path} 出错: {e}")
        
        return docs
    
    #build_knowledge_base() 方法检测文件类型（文本或Excel
    def build_knowledge_base(self, file_pattern: str = None, encoding: str = "utf-8", process_all_files: bool = True) -> bool:
        """
        构建知识库（步骤1-5：数据采集、清洗、拆分、向量化、存储）

        Args:
            file_pattern: 文件匹配模式，如果为None且process_all_files=True，则处理所有支持的文件类型
            encoding: 文件编码，默认"utf-8"
            process_all_files: 是否处理所有支持的文件类型（txt, xlsx, pdf, json, html等），默认True

        Returns:
            是否成功
        """
        try:
            # 明确输出开始构建知识库的信息
            print("\n" + "=" * 80)
            print("开始构建知识库...")
            print(f"知识库目录: {self.knowledge_dir}")
            logger.info("=" * 80)
            logger.info("开始构建知识库...")
            logger.info(f"知识库目录: {self.knowledge_dir}")
            
            docs = []
            
            # 步骤1：加载文档
            if process_all_files and file_pattern is None:
                # 处理所有支持的文件类型
                print("处理所有支持的文件类型...")
                logger.info("处理所有支持的文件类型...")
                
                # 加载文本文件
                txt_docs = self.load_knowledge_base("*.txt", encoding)
                docs.extend(txt_docs)
                print(f"  加载文本文件: {len(txt_docs)} 个文档")
                logger.info(f"  加载文本文件: {len(txt_docs)} 个文档")
                
                # 加载Excel文件
                excel_docs = self.load_excel_files("*.xlsx")
                docs.extend(excel_docs)
                print(f"  加载Excel文件: {len(excel_docs)} 个文档")
                logger.info(f"  加载Excel文件: {len(excel_docs)} 个文档")
                
                excel_docs2 = self.load_excel_files("*.xls")
                docs.extend(excel_docs2)
                print(f"  加载Excel文件(.xls): {len(excel_docs2)} 个文档")
                logger.info(f"  加载Excel文件(.xls): {len(excel_docs2)} 个文档")
                
                # 加载PDF文件
                pdf_docs = self.load_pdf_files("*.pdf")
                docs.extend(pdf_docs)
                print(f"  加载PDF文件: {len(pdf_docs)} 个文档")
                logger.info(f"  加载PDF文件: {len(pdf_docs)} 个文档")
                
                # 加载JSON文件
                json_docs = self.load_json_files("*.json")
                docs.extend(json_docs)
                print(f"  加载JSON文件: {len(json_docs)} 个文档")
                logger.info(f"  加载JSON文件: {len(json_docs)} 个文档")
                
                # 加载HTML文件
                html_docs = self.load_html_files("*.html")
                docs.extend(html_docs)
                print(f"  加载HTML文件: {len(html_docs)} 个文档")
                logger.info(f"  加载HTML文件: {len(html_docs)} 个文档")
                
                html_docs2 = self.load_html_files("*.htm")
                docs.extend(html_docs2)
                print(f"  加载HTML文件(.htm): {len(html_docs2)} 个文档")
                logger.info(f"  加载HTML文件(.htm): {len(html_docs2)} 个文档")
                
                print(f"总计加载: {len(docs)} 个文档")
                logger.info(f"总计加载: {len(docs)} 个文档")
                print("=" * 80 + "\n")
                logger.info("=" * 80)
            else:
                # 根据指定的文件模式加载
                file_pattern = file_pattern or "*.txt"
                print(f"文件模式: {file_pattern}")
                logger.info(f"文件模式: {file_pattern}")
                print("=" * 80 + "\n")
                logger.info("=" * 80)
                
                if file_pattern in ["*.xlsx", "*.xls"]:
                    docs = self.load_excel_files(file_pattern)
                elif file_pattern == "*.pdf":
                    docs = self.load_pdf_files(file_pattern)
                elif file_pattern == "*.json":
                    docs = self.load_json_files(file_pattern)
                elif file_pattern in ["*.html", "*.htm"]:
                    docs = self.load_html_files(file_pattern)
                else:
                    docs = self.load_knowledge_base(file_pattern, encoding)

            if not docs:
                logger.warning("未找到任何文档")
                print("警告: 未找到任何文档，请检查知识库目录中是否有支持的文件")
                return False

            logger.info(f"成功加载 {len(docs)} 个文档")

            # 步骤3：通过 text_splitter.split_documents() 分割文档
            split_docs = self.text_splitter.split_documents(docs)
            logger.info(f"文档分割后共 {len(split_docs)} 个块")

            # ========== 向量化前输出详细信息 ==========
            if split_docs:
                print("\n" + "=" * 80)
                print("向量化前文档信息输出")
                print("=" * 80)
                logger.info("=" * 80)
                logger.info("向量化前文档信息输出")
                logger.info("=" * 80)
                
                # 统计信息
                total_chars = sum(len(doc.page_content) for doc in split_docs)
                avg_chars = total_chars / len(split_docs) if split_docs else 0
                print(f"总文档块数: {len(split_docs)}")
                print(f"总字符数: {total_chars:,}")
                print(f"平均每块字符数: {avg_chars:.2f}")
                logger.info(f"总文档块数: {len(split_docs)}")
                logger.info(f"总字符数: {total_chars:,}")
                logger.info(f"平均每块字符数: {avg_chars:.2f}")
                
                # 按来源统计
                source_stats = {}
                for doc in split_docs:
                    source = doc.metadata.get("source", "未知来源")
                    if source not in source_stats:
                        source_stats[source] = {"count": 0, "total_chars": 0}
                    source_stats[source]["count"] += 1
                    source_stats[source]["total_chars"] += len(doc.page_content)
                
                print("\n按来源统计:")
                logger.info("\n按来源统计:")
                for source, stats in source_stats.items():
                    source_info = f"  {Path(source).name}: {stats['count']} 块, {stats['total_chars']:,} 字符"
                    print(source_info)
                    logger.info(source_info)
                
                # 输出前5个样本的详细信息
                print("\n前5个文档块样本详情:")
                logger.info("\n前5个文档块样本详情:")
                for i, doc in enumerate(split_docs[:5], 1):
                    preview = doc.page_content[:300]
                    sample_info = [
                        f"\n样本 {i}:",
                        f"  长度: {len(doc.page_content)} 字符",
                        f"  来源: {doc.metadata.get('source', '未知来源')}",
                        f"  文件类型: {doc.metadata.get('file_type', '未知')}",
                        f"  加载时间: {doc.metadata.get('load_time', '未知')}",
                        f"  内容预览: {preview}{'...' if len(doc.page_content) > 300 else ''}",
                        f"  完整元数据: {json.dumps(doc.metadata, ensure_ascii=False, indent=2)}"
                    ]
                    for info in sample_info:
                        print(info)
                        logger.info(info)
                
                print("=" * 80)
                print("开始向量化并存储到向量数据库...")
                print("=" * 80 + "\n")
                logger.info("=" * 80)
                logger.info("开始向量化并存储到向量数据库...")
                logger.info("=" * 80)

            # 步骤4-5：使用 vector_store.add_documents() 添加到向量数据库（自动向量化并存储）
            self.vector_store.add_documents(split_docs)
            logger.info(f"知识库构建成功！已向量化并存储 {len(split_docs)} 个文档块")

            return True
        except Exception as e:
            logger.error(f"构建知识库失败: {e}", exc_info=True)
            return False

    # ========== 步骤6：知识库维护 ==========
    
    def update_knowledge_base(self, file_pattern: str = "*.txt", encoding: str = "utf-8") -> bool:
        """
        步骤6：知识库维护 - 定期更新知识库
        
        Args:
            file_pattern: 文件匹配模式
            encoding: 文件编码
        
        Returns:
            是否成功
        """
        logger.info("开始更新知识库...")
        return self.build_knowledge_base(file_pattern, encoding)
    
    def remove_duplicate_chunks(self) -> int:
        """
        步骤6：知识库维护 - 去重优化
        
        Returns:
            删除的重复块数量
        """
        # 注意：ChromaDB的去重需要根据具体实现
        # 这里提供一个占位方法
        logger.info("执行去重优化...")
        return 0

    # ========== 步骤7：用户问题预处理 ==========
    
    def preprocess_query(self, query: str) -> str:
        """
        步骤7：用户问题预处理
        - 纠错：修正错别字
        - 改写：将模糊问题明确化
        
        Args:
            query: 原始问题
        
        Returns:
            预处理后的问题
        """
        if not self.enable_query_preprocessing:
            return query
        
        # 简单的纠错规则（实际可以使用更复杂的纠错模型）
        corrections = {
            "RAG 流成图": "RAG 流程图",
            "怎么用 RAG": "如何搭建基础 RAG 的知识库和检索流程",
        }
        
        for wrong, correct in corrections.items():
            if wrong in query:
                query = query.replace(wrong, correct)
                logger.info(f"问题纠错: {wrong} -> {correct}")
        
        # 去除多余空格
        query = re.sub(r'\s+', ' ', query).strip()
        
        # 如果问题太短，尝试扩展
        if len(query) < 3:
            logger.warning(f"问题过短: {query}")
        
        return query

    # ========== 步骤8-9：问题向量化和向量检索 ==========
    # （这些步骤在semantic_search和hybrid_search中自动完成）

    #使用 vector_store.similarity_search() 进行语义相似度检索
    def semantic_search(self, query: str, k: int = 5, apply_filter: bool = True) -> List[Document]:
        """
        步骤9：向量检索（Top-K）- 语义检索
        
        Args:
            query: 查询文本
            k: 返回的文档数量
            apply_filter: 是否应用过滤（步骤10）
        
        Returns:
            检索到的文档列表
        """
        # 步骤8：问题向量化（在similarity_search_with_score中自动完成）
        results = self.vector_store.similarity_search_with_score(query, k=k * 2)
        
        # 步骤10：检索结果过滤
        if apply_filter:
            filtered_docs = self.filter_retrieval_results(results)
            return filtered_docs[:k]
        else:
            return [doc for doc, score in results[:k]]
    
    def keyword_search(self, query: str, k: int = 5, apply_filter: bool = True) -> List[Document]:
        """
        步骤9：向量检索（Top-K）- 关键词检索
        
        Args:
            query: 查询文本
            k: 返回的文档数量
            apply_filter: 是否应用过滤（步骤10）
        
        Returns:
            检索到的文档列表
        """
        # 使用Chroma的过滤功能进行关键词匹配
        results = self.vector_store.similarity_search_with_score(query, k=k * 2)
        
        # 步骤10：检索结果过滤
        if apply_filter:
            filtered_docs = self.filter_retrieval_results(results)
            return filtered_docs[:k]
        else:
            return [doc for doc, score in results[:k]]

    #hybrid_search() 方法将语义检索和关键词检索的结果合并到一起，并根据权重进行加权
    def hybrid_search(self, query: str, k: int = 5, semantic_weight: float = 0.7, apply_filter: bool = True) -> List[Document]:
        """
        步骤9：向量检索（Top-K）- 混合检索（关键词 + 语义）
        
        Args:
            query: 查询文本
            k: 返回的文档数量
            semantic_weight: 语义检索权重（0-1），关键词检索权重为1-semantic_weight
            apply_filter: 是否应用过滤（步骤10）
        
        Returns:
            检索到的文档列表（去重后）
        """
        # 语义检索
        semantic_results = self.semantic_search(
            query, 
            k=int(k / semantic_weight) if semantic_weight > 0 else k,
            apply_filter=apply_filter
        )
        
        # 关键词检索
        keyword_results = self.keyword_search(
            query, 
            k=int(k / (1 - semantic_weight)) if semantic_weight < 1 else k,
            apply_filter=apply_filter
        )
        
        # 合并结果并去重
        seen_contents = set()
        combined_results = []
        
        # 优先添加语义检索结果
        for doc in semantic_results:
            content_hash = self._calculate_document_hash(doc.page_content)
            if content_hash not in seen_contents:
                seen_contents.add(content_hash)
                combined_results.append(doc)
        
        # 添加关键词检索结果
        for doc in keyword_results:
            content_hash = self._calculate_document_hash(doc.page_content)
            if content_hash not in seen_contents:
                seen_contents.add(content_hash)
                combined_results.append(doc)
        
        return combined_results[:k]

    # ========== 步骤10：检索结果过滤 ==========
    
    def filter_retrieval_results(
        self, 
        results: List[Tuple[Document, float]], 
        threshold: Optional[float] = None
    ) -> List[Document]:
        """
        步骤10：检索结果过滤
        - 相关性阈值：过滤向量距离超过阈值的Chunk
        - 去重：合并内容重复的检索结果
        
        Args:
            results: 检索结果列表，每个元素为(文档, 相似度分数)
            threshold: 相关性阈值，默认使用self.similarity_threshold
        
        Returns:
            过滤后的文档列表
        """
        if not results:
            return []
        
        threshold = threshold or self.similarity_threshold
        
        # 过滤低相关性结果
        # 注意：ChromaDB的similarity_search_with_score返回的分数是距离（越小越相似）
        # 这里假设分数范围是0-1，分数越小表示越相似
        filtered_results = []
        seen_contents = set()
        
        print(f"[调试] 开始过滤检索结果，原始数量: {len(results)}, 阈值: {threshold}")
        logger.info(f"[调试] 开始过滤检索结果，原始数量: {len(results)}, 阈值: {threshold}")
        
        for doc, score in results:
            # 转换为相似度（假设score是距离，转换为相似度）
            # 如果score已经是相似度（越大越相似），则直接比较
            similarity = 1 - score if score <= 1 else 1 / (1 + score)
            
            # 相关性阈值过滤
            if similarity >= threshold:
                # 去重：检查内容是否重复
                content_hash = self._calculate_document_hash(doc.page_content)
                if content_hash not in seen_contents:
                    seen_contents.add(content_hash)
                    filtered_results.append(doc)
            else:
                # 调试：显示被过滤的结果
                if len(filtered_results) < 3:  # 只显示前几个被过滤的
                    print(f"[调试] 结果被过滤: 距离={score:.4f}, 相似度={similarity:.4f} < 阈值{threshold}")
                    logger.debug(f"[调试] 结果被过滤: 距离={score:.4f}, 相似度={similarity:.4f} < 阈值{threshold}")
        
        print(f"[调试] 过滤后数量: {len(filtered_results)} (过滤掉 {len(results) - len(filtered_results)} 个)")
        logger.info(f"检索结果过滤: {len(results)} -> {len(filtered_results)} (阈值: {threshold})")
        return filtered_results


    # ========== 步骤11：Prompt构建 ==========
    
    def build_prompt(self, context: str, question: str, sources: List[Document] = None) -> str:
        """
        步骤11：Prompt构建
        将"问题 + 检索知识"组织成LLM能理解的格式
        
        Args:
            context: 检索到的上下文
            question: 用户问题
            sources: 来源文档列表（用于生成引用）
        
        Returns:
            构建好的提示词
        """
        # 构建上下文信息
        context_parts = [context]
        
        # 如果有来源信息，添加到上下文中
        if sources:
            source_info = []
            for i, doc in enumerate(sources[:3], 1):  # 只显示前3个来源
                source = doc.metadata.get('source', '未知来源')
                source_info.append(f"来源{i}: {source}")
            context_parts.append("\n".join(source_info))
        
        full_context = "\n\n".join(context_parts)
        
        # 使用模板构建提示词
        prompt = self.prompt_template.format(context=full_context, question=question)
        return prompt
    
    # ========== 步骤13：结果优化 ==========
    
    def optimize_result(self, answer: str, context: str, sources: List[Document] = None) -> Dict[str, Any]:
        """
        步骤13：结果优化
        - 事实校验：检查回答是否与检索知识一致
        - 格式调整：按用户需求输出
        
        Args:
            answer: LLM生成的回答
            context: 检索到的上下文
            sources: 来源文档列表
        
        Returns:
            优化后的结果字典，包含answer、sources等信息
        """
        if not self.enable_result_optimization:
            return {
                "answer": answer,
                "sources": self._extract_sources(sources) if sources else []
            }
        
        # 简单的事实校验：检查回答中是否包含上下文中的关键信息
        # 实际可以使用更复杂的校验方法
        context_keywords = set(re.findall(r'[\u4e00-\u9fa5]{2,}', context))
        answer_keywords = set(re.findall(r'[\u4e00-\u9fa5]{2,}', answer))
        
        # 计算关键词重叠率
        if context_keywords:
            overlap_ratio = len(answer_keywords & context_keywords) / len(context_keywords)
            logger.info(f"事实校验 - 关键词重叠率: {overlap_ratio:.2%}")
        
        # 格式调整：确保回答格式良好
        answer = answer.strip()
        # 去除多余的空行
        answer = re.sub(r'\n{3,}', '\n\n', answer)
        
        return {
            "answer": answer,
            "sources": self._extract_sources(sources) if sources else []
        }
    
    def _extract_sources(self, sources: List[Document]) -> List[Dict[str, str]]:
        """提取来源信息"""
        source_list = []
        for doc in sources:
            metadata = doc.metadata or {}
            source_info = {
                "source": metadata.get('source', '未知来源'),
                "file_type": metadata.get('file_type', '未知类型'),
                "page": metadata.get('page', ''),
            }
            source_list.append(source_info)
        return source_list

    # ========== 步骤14：输出最终回答 ==========
    
    def format_final_answer(self, answer: str, sources: List[Dict[str, str]] = None) -> str:
        """
        步骤14：输出最终回答
        向用户返回清晰、准确的结果，附带"引用来源"
        
        Args:
            answer: 优化后的回答
            sources: 来源信息列表
        
        Returns:
            格式化的最终回答
        """
        result = answer
        
        # 如果有来源信息，添加到回答末尾
        if sources:
            source_text = "\n\n参考知识来源：\n"
            for i, source in enumerate(sources[:3], 1):  # 只显示前3个来源
                source_name = Path(source['source']).name if source.get('source') else '未知来源'
                source_text += f"{i}. {source_name}"
                if source.get('page'):
                    source_text += f" (第{source['page']}页)"
                source_text += "\n"
            result += source_text
        
        return result

    #检索增强生成 - 完整14步流程
    def query(
        self, 
        question: str, 
        use_hybrid: bool = True,
        return_sources: bool = True
    ) -> Dict[str, Any]:
        """
        完整RAG查询流程（14步）
        
        Args:
            question: 用户问题
            use_hybrid: 是否使用混合检索
            return_sources: 是否返回来源信息
        
        Returns:
            包含answer和sources的字典
        """
        # 步骤7：用户问题预处理
        processed_question = self.preprocess_query(question)
        
        # 调试：检查向量数据库中的文档数量
        try:
            collection = self.vector_store._collection
            doc_count = collection.count() if collection else 0
            print(f"\n[调试] 向量数据库中的文档总数: {doc_count}")
            logger.info(f"[调试] 向量数据库中的文档总数: {doc_count}")
        except Exception as e:
            print(f"[调试] 无法获取文档数量: {e}")
            logger.warning(f"[调试] 无法获取文档数量: {e}")
        
        # 步骤8-9：问题向量化和向量检索
        print(f"\n[调试] 开始检索，问题: {question}")
        print(f"[调试] 使用混合检索: {use_hybrid}")
        logger.info(f"[调试] 开始检索，问题: {question}")
        logger.info(f"[调试] 使用混合检索: {use_hybrid}")
        
        # 先获取未过滤的检索结果，查看原始相似度分数
        try:
            raw_results = self.vector_store.similarity_search_with_score(processed_question, k=10)
            print(f"[调试] 原始检索结果数量: {len(raw_results)}")
            logger.info(f"[调试] 原始检索结果数量: {len(raw_results)}")
            if raw_results:
                print(f"[调试] 前3个结果的相似度分数（距离，越小越相似）:")
                logger.info(f"[调试] 前3个结果的相似度分数（距离，越小越相似）:")
                for i, (doc, score) in enumerate(raw_results[:3], 1):
                    similarity = 1 - score if score <= 1 else 1 / (1 + score)
                    print(f"  结果{i}: 距离={score:.4f}, 相似度={similarity:.4f}, 阈值={self.similarity_threshold}")
                    print(f"    内容预览: {doc.page_content[:100]}...")
                    logger.info(f"  结果{i}: 距离={score:.4f}, 相似度={similarity:.4f}, 阈值={self.similarity_threshold}")
                    logger.info(f"    内容预览: {doc.page_content[:100]}...")
        except Exception as e:
            print(f"[调试] 获取原始检索结果失败: {e}")
            logger.warning(f"[调试] 获取原始检索结果失败: {e}")
        
        if use_hybrid:
            docs = self.hybrid_search(processed_question, k=5, apply_filter=True)
        else:
            docs = self.semantic_search(processed_question, k=5, apply_filter=True)
        
        print(f"[调试] 过滤后检索到的文档数量: {len(docs)}")
        logger.info(f"[调试] 过滤后检索到的文档数量: {len(docs)}")
        
        if docs:
            print(f"[调试] 检索到的文档内容预览:")
            logger.info(f"[调试] 检索到的文档内容预览:")
            for i, doc in enumerate(docs[:3], 1):
                print(f"  文档{i}: {doc.page_content[:150]}...")
                logger.info(f"  文档{i}: {doc.page_content[:150]}...")
        
        # 构建上下文
        context = "\n\n".join([doc.page_content for doc in docs])
        
        if not context:
            print(f"[调试] 警告: 检索结果为空，可能原因:")
            print(f"  1. 相似度阈值过高 (当前: {self.similarity_threshold})")
            print(f"  2. 知识库未正确构建")
            print(f"  3. 查询问题与知识库内容不匹配")
            logger.warning(f"[调试] 检索结果为空，相似度阈值: {self.similarity_threshold}")
            return {
                "answer": "抱歉，我在知识库中没有找到相关信息。",
                "sources": []
            }
        
        # 步骤11：Prompt构建
        prompt = self.build_prompt(context, processed_question, docs if return_sources else None)
        
        # 步骤12：LLM生成回答
        raw_answer = self.llm.invoke(prompt)
        
        # 步骤13：结果优化
        optimized_result = self.optimize_result(raw_answer, context, docs if return_sources else None)
        
        # 步骤14：输出最终回答
        if return_sources:
            final_answer = self.format_final_answer(
                optimized_result["answer"], 
                optimized_result["sources"]
            )
        else:
            final_answer = optimized_result["answer"]
        
        return {
            "answer": final_answer,
            "sources": optimized_result["sources"] if return_sources else []
        }
    
    def query_with_chain(self, question: str, return_dict: bool = False) -> Any:
        """
        使用处理链进行查询（推荐方式）
        
        Args:
            question: 用户问题
            return_dict: 是否返回字典格式（包含sources）
        
        Returns:
            回答文本或字典
        """
        try:
            # 步骤7：用户问题预处理
            processed_question = self.preprocess_query(question)
            
            # 使用处理链
            result = self.chain.invoke(processed_question)
            # 如果返回的是字符串，直接返回
            if isinstance(result, str):
                answer = result
            else:
                answer = StrOutputParser().parse(result)
            
            if return_dict:
                # 如果需要返回字典格式，执行完整的14步流程
                return self.query(question, use_hybrid=True, return_sources=True)
            else:
                return answer
        except Exception as e:
            logger.error(f"链式调用失败: {e}", exc_info=True)
            # 如果链式调用失败，回退到直接查询
            result = self.query(question, use_hybrid=True, return_sources=return_dict)
            return result if return_dict else result.get("answer", "抱歉，处理失败。")
    
    def add_documents(self, documents: List) -> None:
        """
        添加文档到知识库
        Args:
            documents: 文档列表
        """
        split_docs = self.text_splitter.split_documents(documents)
        
        # ========== 向量化前输出详细信息 ==========
        if split_docs:
            print("\n" + "=" * 80)
            print("向量化前文档信息输出（add_documents）")
            print("=" * 80)
            logger.info("=" * 80)
            logger.info("向量化前文档信息输出（add_documents）")
            logger.info("=" * 80)
            
            # 统计信息
            total_chars = sum(len(doc.page_content) for doc in split_docs)
            avg_chars = total_chars / len(split_docs) if split_docs else 0
            print(f"新增文档块数: {len(split_docs)}")
            print(f"总字符数: {total_chars:,}")
            print(f"平均每块字符数: {avg_chars:.2f}")
            logger.info(f"新增文档块数: {len(split_docs)}")
            logger.info(f"总字符数: {total_chars:,}")
            logger.info(f"平均每块字符数: {avg_chars:.2f}")
            
            # 按来源统计
            source_stats = {}
            for doc in split_docs:
                source = doc.metadata.get("source", "未知来源")
                if source not in source_stats:
                    source_stats[source] = {"count": 0, "total_chars": 0}
                source_stats[source]["count"] += 1
                source_stats[source]["total_chars"] += len(doc.page_content)
            
            print("\n按来源统计:")
            logger.info("\n按来源统计:")
            for source, stats in source_stats.items():
                source_info = f"  {Path(source).name}: {stats['count']} 块, {stats['total_chars']:,} 字符"
                print(source_info)
                logger.info(source_info)
            
            # 输出前3个样本的详细信息
            print("\n前3个文档块样本详情:")
            logger.info("\n前3个文档块样本详情:")
            for i, doc in enumerate(split_docs[:3], 1):
                preview = doc.page_content[:300]
                sample_info = [
                    f"\n样本 {i}:",
                    f"  长度: {len(doc.page_content)} 字符",
                    f"  来源: {doc.metadata.get('source', '未知来源')}",
                    f"  文件类型: {doc.metadata.get('file_type', '未知')}",
                    f"  内容预览: {preview}{'...' if len(doc.page_content) > 300 else ''}"
                ]
                for info in sample_info:
                    print(info)
                    logger.info(info)
            
            print("=" * 80)
            print("开始向量化并存储到向量数据库...")
            print("=" * 80 + "\n")
            logger.info("=" * 80)
            logger.info("开始向量化并存储到向量数据库...")
            logger.info("=" * 80)
        
        self.vector_store.add_documents(split_docs)
        logger.info(f"文档添加成功！已向量化并存储 {len(split_docs)} 个文档块")
    
    def is_knowledge_base_empty(self) -> bool:
        """
        检查知识库是否为空
        
        Returns:
            True表示知识库为空，False表示有数据
        """
        try:
            collection = self.vector_store._collection
            if collection:
                doc_count = collection.count()
                return doc_count == 0
            return True
        except Exception as e:
            logger.warning(f"检查知识库状态失败: {e}")
            return True  # 如果检查失败，假设为空，触发构建
    
    def delete_knowledge_base(self) -> None:
        """清空知识库"""
        # 注意：ChromaDB的删除操作需要根据具体实现
        # 这里提供一个占位方法
        pass


# 全局RAG服务实例（单例模式）
_rag_service_instance: Optional[RAGService] = None


def get_rag_service() -> RAGService:
    """
    获取RAG服务实例（单例）
    
    Returns:
        RAGService实例
    """
    global _rag_service_instance
    if _rag_service_instance is None:
        _rag_service_instance = RAGService()
    return _rag_service_instance


def init_rag_service(
    knowledge_dir: str = "my_knowledge",
    persist_directory: str = "./chroma_v4",
    **kwargs
) -> RAGService:
    """
    初始化RAG服务
    
    Args:
        knowledge_dir: 知识库目录
        persist_directory: 向量数据库目录
        **kwargs: 其他参数
    
    Returns:
        RAGService实例
    """
    global _rag_service_instance
    _rag_service_instance = RAGService(
        knowledge_dir=knowledge_dir,
        persist_directory=persist_directory,
        **kwargs
    )
    return _rag_service_instance

